package com.gmcc.aiquality.webadmin.upms.controller;

import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONArray;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.gmcc.aiquality.webadmin.upms.dao.SysDataPermUserMapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import com.gmcc.aiquality.webadmin.config.ApplicationConfig;
import com.gmcc.aiquality.webadmin.upms.bo.*;
import com.gmcc.aiquality.webadmin.upms.service.*;
import com.gmcc.aiquality.webadmin.upms.model.*;
import com.gmcc.aiquality.webadmin.upms.model.constant.SysUserStatus;
import com.gmcc.aiquality.webadmin.upms.model.constant.SysUserType;
import com.gmcc.aiquality.webadmin.upms.model.constant.SysMenuType;
import com.gmcc.aiquality.webadmin.upms.model.constant.SysOnlineMenuPermType;
import com.gmcc.aiquality.common.flow.online.service.FlowOnlineOperationService;
import com.gmcc.aiquality.common.online.service.OnlineOperationService;
import com.gmcc.aiquality.common.report.model.ReportPage;
import com.gmcc.aiquality.common.report.service.ReportOperationService;
import com.gmcc.aiquality.common.report.service.ReportPageService;
import com.gmcc.aiquality.common.core.annotation.MyRequestBody;
import com.gmcc.aiquality.common.core.annotation.DisableDataFilter;
import com.gmcc.aiquality.common.core.constant.ErrorCodeEnum;
import com.gmcc.aiquality.common.core.constant.ApplicationConstant;
import com.gmcc.aiquality.common.core.object.*;
import com.gmcc.aiquality.common.core.util.*;
import com.gmcc.aiquality.common.core.upload.*;
import com.gmcc.aiquality.common.redis.cache.SessionCacheHelper;
import com.gmcc.aiquality.common.log.annotation.OperationLog;
import com.gmcc.aiquality.common.log.model.constant.SysOperationLogType;
import com.gmcc.aiquality.common.satoken.util.SaTokenUtil;
import org.redisson.api.RSet;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 登录接口控制器类。
 *
 * @author 孙先生
 * @date 2025-10-23
 */
@ApiSupport(order = 1)
@Tag(name = "用户登录接口")
@DisableDataFilter
@Slf4j
@RestController
@RequestMapping("/admin/upms/login")
public class LoginController {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysDeptService sysDeptService;
    @Autowired
    private SysMenuService sysMenuService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysDataPermService sysDataPermService;
    @Autowired
    private SysPermWhitelistService sysPermWhitelistService;
    @Autowired
    private SysConfService sysConfService;
    @Autowired
    private OnlineOperationService onlineOperationService;
    @Autowired
    private FlowOnlineOperationService flowOnlineOperationService;
    @Autowired
    private ReportOperationService reportOperationService;
    @Autowired
    private ReportPageService reportPageService;
    @Autowired
    private ApplicationConfig appConfig;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private SessionCacheHelper cacheHelper;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UpDownloaderFactory upDownloaderFactory;
    @Autowired
    private SaTokenUtil saTokenUtil;



    private static final String IS_ADMIN = "isAdmin";
    private static final String SHOW_NAME_FIELD = "showName";
    private static final String SHOW_ORDER_FIELD = "showOrder";
    private static final String HEAD_IMAGE_URL_FIELD = "headImageUrl";

    /**
     * 登录接口。
     *
     * @param loginName 登录名。
     * @param password  密码。
     * @return 应答结果对象，其中包括Token数据，以及菜单列表。
     */
    @Parameter(name = "loginName", example = "admin")
    @Parameter(name = "password", example = "IP3ccke3GhH45iGHB5qP9p7iZw6xUyj28Ju10rnBiPKOI35sc%2BjI7%2FdsjOkHWMfUwGYGfz8ik31HC2Ruk%2Fhkd9f6RPULTHj7VpFdNdde2P9M4mQQnFBAiPM7VT9iW3RyCtPlJexQ3nAiA09OqG%2F0sIf1kcyveSrulxembARDbDo%3D")
    @SaIgnore
    @OperationLog(type = SysOperationLogType.LOGIN, saveResponse = false)
    @PostMapping("/doLogin")
    public ResponseResult<JSONObject> doLogin(
            @MyRequestBody String loginName,
            @MyRequestBody String password) throws UnsupportedEncodingException {
        if (MyCommonUtil.existBlankArgument(loginName, password)) {
            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
        }
        ResponseResult<SysUser> verifyResult = this.verifyAndHandleLoginUser(loginName, password);
        if (!verifyResult.isSuccess()) {
            return ResponseResult.errorFrom(verifyResult);
        }
        JSONObject jsonData = this.buildLoginDataAndLogin(verifyResult.getData());
        return ResponseResult.success(jsonData);
    }

    /**
     * 登出操作。同时将Session相关的信息从缓存中删除。
     *
     * @return 应答结果对象。
     */
    @OperationLog(type = SysOperationLogType.LOGOUT)
    @PostMapping("/doLogout")
    public ResponseResult<Void> doLogout() {
        String sessionId = TokenData.takeFromRequest().getSessionId();
        redissonClient.getBucket(TokenData.takeFromRequest().getMySessionId()).deleteAsync();
        redissonClient.getBucket(RedisKeyUtil.makeSessionPermCodeKey(sessionId)).deleteAsync();
        redissonClient.getBucket(RedisKeyUtil.makeSessionPermIdKey(sessionId)).deleteAsync();
        sysDataPermService.removeDataPermCache(sessionId);
        cacheHelper.removeAllSessionCache(sessionId);
        StpUtil.logout();
        return ResponseResult.success();
    }

    /**
     * 在登录之后，通过token再次获取登录信息。
     * 用于在当前浏览器登录系统后，在新tab页中可以免密登录。
     *
     * @return 应答结果对象，其中包括JWT的Token数据，以及菜单列表。
     */
    @GetMapping("/getLoginInfo")
    public ResponseResult<JSONObject> getLoginInfo() {
        TokenData tokenData = TokenData.takeFromRequest();
        JSONObject jsonData = new JSONObject();
        jsonData.put(SHOW_NAME_FIELD, tokenData.getShowName());
        jsonData.put(IS_ADMIN, tokenData.getIsAdmin());
        jsonData.put("userId", tokenData.getUserId());
        jsonData.put("loginName", tokenData.getLoginName());
        jsonData.put("deptId", tokenData.getDeptId());
        jsonData.put("deptName", tokenData.getDeptName());
        if (StrUtil.isNotBlank(tokenData.getHeadImageUrl())) {
            jsonData.put(HEAD_IMAGE_URL_FIELD, tokenData.getHeadImageUrl());
        }
        Collection<SysMenu> allMenuList;
        if (BooleanUtil.isTrue(tokenData.getIsAdmin())) {
            allMenuList = sysMenuService.getAllListByOrder(SHOW_ORDER_FIELD);
        } else {
            allMenuList = sysMenuService.getMenuListByRoleIds(tokenData.getRoleIds());
        }
        List<String> menuCodeList = new LinkedList<>();
        OnlinePermData onlinePermData = this.getOnlineMenuPermData(allMenuList);
        CollUtil.addAll(menuCodeList, onlinePermData.permCodeSet);
        OnlinePermData onlineFlowPermData = this.getFlowOnlineMenuPermData(allMenuList);
        CollUtil.addAll(menuCodeList, onlineFlowPermData.permCodeSet);
        allMenuList.stream().filter(m -> m.getExtraData() != null)
                .forEach(m -> m.setExtraObject(JSON.parseObject(m.getExtraData(), SysMenuExtraData.class)));
        this.appendResponseMenuAndPermCodeData(jsonData, allMenuList, menuCodeList);
        return ResponseResult.success(jsonData);
    }

    /**
     * 返回所有可用的权限字列表。
     * 
     * @return 整个系统所有可用的权限字列表。
     */
    @GetMapping("/getAllPermCodes")
    public ResponseResult<List<String>> getAllPermCodes() {
        List<String> permCodes = saTokenUtil.getAllPermCodes();
        return ResponseResult.success(permCodes);
    }

    /**
     * 在没有登录的情况下，用户修改自己的密码。
     *
     * @param loginName 原有密码。
     * @param oldPass   原有密码。
     * @param newPass   新密码。
     * @return 应答结果对象。
     */
    @SaIgnore
    @PostMapping("/changePasswordWithoutLogin")
    public ResponseResult<Void> changePasswordWithoutLogin(
            @MyRequestBody(required = true) String loginName,
            @MyRequestBody(required = true) String oldPass,
            @MyRequestBody(required = true) String newPass) throws UnsupportedEncodingException {
        SysUser user = sysUserService.getSysUserByLoginName(loginName);
        return this.doChangePassword(user, oldPass, newPass);
    }

    /**
     * 用户修改自己的密码。
     *
     * @param oldPass 原有密码。
     * @param newPass 新密码。
     * @return 应答结果对象。
     */
    @PostMapping("/changePassword")
    public ResponseResult<Void> changePassword(
            @MyRequestBody(required = true) String oldPass,
            @MyRequestBody(required = true) String newPass) throws UnsupportedEncodingException {
        SysUser user = sysUserService.getById(TokenData.takeFromRequest().getUserId());
        return this.doChangePassword(user, oldPass, newPass);
    }
    
    /**
     * 上传并修改用户头像。
     *
     * @param uploadFile 上传的头像文件。
     */
    @PostMapping("/changeHeadImage")
    public void changeHeadImage(@RequestParam("uploadFile") MultipartFile uploadFile) throws IOException {
        UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, HEAD_IMAGE_URL_FIELD);
        BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
        UploadResponseInfo responseInfo = upDownloader.doUpload(null,
                appConfig.getUploadFileBaseDir(), SysUser.class.getSimpleName(), HEAD_IMAGE_URL_FIELD, true, uploadFile);
        if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) {
            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
                    ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage()));
            return;
        }
        responseInfo.setDownloadUri("/admin/upms/login/downloadHeadImage");
        String newHeadImage = JSONArray.toJSONString(CollUtil.newArrayList(responseInfo));
        if (!sysUserService.changeHeadImage(TokenData.takeFromRequest().getUserId(), newHeadImage)) {
            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST));
            return;
        }
        ResponseResult.output(ResponseResult.success(responseInfo));
    }

    /**
     * 修改工作台Id。
     *
     * @param newWorkbenchId 新的工作台Id。
     * @return 修改应答结果
     */
    @GetMapping("/changeWorkbenchId")
    public ResponseResult<Void> changeWorkbenchId(@RequestParam Long newWorkbenchId) {
        TokenData tokenData = TokenData.takeFromRequest();
        if (!CollUtil.contains(tokenData.getWorkbenchIds(), newWorkbenchId)) {
            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, "数据验证失败，当前工作台Id不存在！");
        }
        sysUserService.changeWorkbenchId(tokenData.getUserId(), newWorkbenchId);
        return ResponseResult.success();
    }

    /**
     * 修改用户签名图片。
     *
     * @param signingPicture 签名图片。
     * @return 应答结果对象，无返回数据。
     */
    @PostMapping("/changeSigningPicture")
    public ResponseResult<Void> changeSigningPicture(@MyRequestBody String signingPicture) {
        sysUserService.changeSigningPicture(signingPicture);
        return ResponseResult.success();
    }

    /**
     * 获取用户签名图片。
     *
     * @return 应答结果对象，返回数据为签名图片。
     */
    @GetMapping("/getSigningPicture")
    public ResponseResult<String> getSigningPicture() {
        return ResponseResult.success(sysUserService.getSigningPicture());
    }

    /**
     * 下载用户头像。
     *
     * @param filename 文件名。如果没有提供该参数，就从当前记录的指定字段中读取。
     * @param response Http 应答对象。
     */
    @GetMapping("/downloadHeadImage")
    public void downloadHeadImage(String filename, HttpServletResponse response) {
        try {
            UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, HEAD_IMAGE_URL_FIELD);
            BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
            upDownloader.doDownload(appConfig.getUploadFileBaseDir(),
                    SysUser.class.getSimpleName(), HEAD_IMAGE_URL_FIELD, filename, true, response);
        } catch (Exception e) {
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            log.error(e.getMessage(), e);
        }
    }

    private ResponseResult<SysUser> verifyAndHandleLoginUser(
            String loginName, String password) throws UnsupportedEncodingException {
        String errorMessage;
        SysUser user = sysUserService.getSysUserByLoginName(loginName);
        if (user == null) {
            errorMessage = "登录失败，当前用户不存在或密码错误！";
            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
        }
        if (user.getUserStatus() == SysUserStatus.STATUS_LOCKED) {
            errorMessage = "登录失败，用户账号被锁定！";
            return ResponseResult.error(ErrorCodeEnum.INVALID_USER_STATUS, errorMessage);
        }
        SysConfLoginPolicy policy = this.getLoginPolicyFromSysConf();
        if (this.isLoginDelayLocked(loginName)) {
            errorMessage = "登录失败，密码连续错误次数超出限制，请 " + policy.getDelayLoginTime() + " 分钟后再试！";
            return ResponseResult.error(ErrorCodeEnum.INVALID_USER_STATUS, errorMessage);
        }
        // NOTE: 第一次使用时，请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。
        // 执行RsaUtil工具类中的main函数，可以生成新的公钥和私钥。
        password = URLDecoder.decode(password, StandardCharsets.UTF_8.name());
        password = RsaUtil.decrypt(password, ApplicationConstant.PRIVATE_KEY);
        if (!passwordEncoder.matches(password, user.getPassword())) {
            if (policy != null) {
                this.handlePasswordErrorWhenLogin(loginName, policy);
            }
            return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD);
        }
        this.clearLoginError(loginName);
        this.excludeLogin(user, policy);
        // 验证是否需要重新设置密码
        CallResult checkResult = this.checkChangePasswordPolicy(user);
        if (!checkResult.isSuccess()) {
            return ResponseResult.error(ErrorCodeEnum.FORCE_RESET_PASSWORD, checkResult.getErrorMessage());
        }
        return ResponseResult.success(user);
    }

    private SysConfLoginPolicy getLoginPolicyFromSysConf() {
        String key = "loginPolicy";
        HttpServletRequest request = ContextUtil.getHttpRequest();
        SysConfLoginPolicy loginPolicy = (SysConfLoginPolicy) request.getAttribute(key);
        if (loginPolicy != null) {
            return loginPolicy;
        }
        SysConf conf = sysConfService.getSysConfByCode(key);
        loginPolicy = conf == null ? null : JSON.parseObject(conf.getConf(), SysConfLoginPolicy.class);
        request.setAttribute(key, loginPolicy);
        return loginPolicy;
    }

    private void excludeLogin(SysUser user, SysConfLoginPolicy policy) {
        if (policy != null && BooleanUtil.isTrue(policy.getExcludeLogin())) {
            String deviceType = MyCommonUtil.getDeviceTypeWithString();
            LoginUserInfo userInfo = BeanUtil.copyProperties(user, LoginUserInfo.class);
            String loginId = SaTokenUtil.makeLoginId(userInfo);
            StpUtil.kickout(loginId, deviceType);
        }
    }

    private boolean isLoginDelayLocked(String loginName) {
        String key = RedisKeyUtil.makeLoginLockedKey(loginName);
        return redissonClient.getBucket(key).isExists();
    }

    private void handlePasswordErrorWhenLogin(String loginName, SysConfLoginPolicy policy) {
        if (policy.getPasswordErrorCount() <= 0) {
            return;
        }
        String failCountKey = RedisKeyUtil.makeLoginFailCountKey(loginName);
        long count = redissonClient.getAtomicLong(failCountKey).incrementAndGet();
        if (count >= policy.getPasswordErrorCount()) {
            if (BooleanUtil.isTrue(policy.getDelayLoginWhenError())) {
                String key = RedisKeyUtil.makeLoginLockedKey(loginName);
                redissonClient.getBucket(key).set("LOCKED", policy.getDelayLoginTime(), TimeUnit.MINUTES);
            } else {
                SysUser user = sysUserService.getSysUserByLoginName(loginName);
                if (user != null) {
                    SysUser updatedUser = new SysUser();
                    updatedUser.setUserId(user.getUserId());
                    updatedUser.setUserStatus(SysUserStatus.STATUS_LOCKED);
                    sysUserService.updateById(updatedUser);
                }
            }
            redissonClient.getAtomicLong(failCountKey).delete();
        }
    }

    private void clearLoginError(String loginName) {
        redissonClient.getAtomicLong(RedisKeyUtil.makeLoginFailCountKey(loginName)).delete();
        redissonClient.getBucket(RedisKeyUtil.makeLoginLockedKey(loginName)).delete();
    }

    private CallResult checkChangePasswordPolicy(SysUser user) {
        SysConf conf = sysConfService.getSysConfByCode("passwordPolicy");
        if (conf == null) {
            return CallResult.ok();
        }
        SysConfPasswordPolicy policy = JSON.parseObject(conf.getConf(), SysConfPasswordPolicy.class);
        if (Boolean.TRUE.equals(policy.getPasswordExpired())) {
            if (user.getPasswordUpdateTime() == null) {
                return CallResult.error("您的密码已过期，请先修改密码后登录！");
            }
            long days = (System.currentTimeMillis() - user.getPasswordUpdateTime().getTime()) / (1000 * 3600 * 24);
            if (days > policy.getUpdateInterval()) {
                return CallResult.error("您的密码已过期，请先修改密码后登录！");
            }
        }
        if (Boolean.TRUE.equals(policy.getModifyInitPassword())
                && passwordEncoder.matches(policy.getInitPassword(), user.getPassword())) {
            return CallResult.error("您的密码与初始化密码相同，请修改您的密码！");
        }
        return CallResult.ok();
    }

    private int calcSessionExpiredSeconds() {
        SysConfLoginPolicy loginPolicy = this.getLoginPolicyFromSysConf();
        if (loginPolicy != null && loginPolicy.getLoginTimeout() != null && loginPolicy.getLoginTimeout() > 0) {
            return loginPolicy.getLoginTimeout() * 60;
        }
        return appConfig.getSessionExpiredSeconds();
    }

    private ResponseResult<Void> doChangePassword(
            SysUser user, String oldPass, String newPass) throws UnsupportedEncodingException {
        oldPass = URLDecoder.decode(oldPass, StandardCharsets.UTF_8.name());
        // NOTE: 第一次使用时，请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。
        // 执行RsaUtil工具类中的main函数，可以生成新的公钥和私钥。
        oldPass = RsaUtil.decrypt(oldPass, ApplicationConstant.PRIVATE_KEY);
        if (user == null || !passwordEncoder.matches(oldPass, user.getPassword())) {
            return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD);
        }
        newPass = URLDecoder.decode(newPass, StandardCharsets.UTF_8.name());
        newPass = RsaUtil.decrypt(newPass, ApplicationConstant.PRIVATE_KEY);
        if (!sysUserService.changePassword(user.getUserId(), newPass)) {
            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
        }
        return ResponseResult.success();
    }

    private JSONObject buildLoginDataAndLogin(SysUser user) {
        TokenData tokenData = this.loginAndCreateToken(user);
        // 这里手动将TokenData存入request，便于OperationLogAspect统一处理操作日志。
        TokenData.addToRequest(tokenData);
        JSONObject jsonData = this.createResponseData(user, tokenData);
        Collection<SysMenu> allMenuList;
        boolean isAdmin = user.getUserType() == SysUserType.TYPE_ADMIN;
        if (isAdmin) {
            allMenuList = sysMenuService.getAllListByOrder(SHOW_ORDER_FIELD);
        } else {
            allMenuList = sysMenuService.getMenuListByRoleIds(tokenData.getRoleIds());
        }
        allMenuList.stream().filter(m -> m.getExtraData() != null)
                .forEach(m -> m.setExtraObject(JSON.parseObject(m.getExtraData(), SysMenuExtraData.class)));
        this.cacheTokenDataWithFlowEntryIds(allMenuList, tokenData);
        Collection<String> permCodeList = new LinkedList<>();
        allMenuList.stream().filter(m -> m.getExtraObject() != null)
                .forEach(m -> CollUtil.addAll(permCodeList, m.getExtraObject().getPermCodeList()));
        Set<String> permSet = new HashSet<>();
        if (!isAdmin) {
            // 所有登录用户都有白名单接口的访问权限。
            CollUtil.addAll(permSet, sysPermWhitelistService.getWhitelistPermList());
        }
        List<String> menuCodeList = new LinkedList<>();
        OnlinePermData onlinePermData = this.getOnlineMenuPermData(allMenuList);
        CollUtil.addAll(menuCodeList, onlinePermData.permCodeSet);
        OnlinePermData onlineFlowPermData = this.getFlowOnlineMenuPermData(allMenuList);
        CollUtil.addAll(menuCodeList, onlineFlowPermData.permCodeSet);
        if (!isAdmin) {
            permSet.addAll(onlinePermData.permUrlSet);
            permSet.addAll(onlineFlowPermData.permUrlSet);
            Set<String> reportPermSet = this.getReportMenuPermData(allMenuList);
            permSet.addAll(reportPermSet);
            String sessionId = tokenData.getSessionId();
            // 缓存用户的权限资源，这里缓存的是基于URL验证的权限资源，比如在线表单、工作流和数据表中的白名单资源。
            this.putUserSysPermCache(sessionId, permSet);
            // 缓存权限字字段，StpInterfaceImpl中会从缓存中读取，并交给satoken进行接口权限的验证。
            this.putUserSysPermCodeCache(sessionId, permCodeList);
            sysDataPermService.putDataPermCache(sessionId, user.getUserId(), user.getDeptId());
        }
        this.appendResponseMenuAndPermCodeData(jsonData, allMenuList, menuCodeList);
        return jsonData;
    }

    private void cacheTokenDataWithFlowEntryIds(Collection<SysMenu> allMenuList, TokenData tokenData) {
        allMenuList.stream().filter(m -> m.getExtraObject() != null)
                .filter(m -> m.getExtraObject().getOnlineFlowEntryId() != null)
                .forEach(m -> tokenData.getFlowEntryIds().add(m.getExtraObject().getOnlineFlowEntryId()));
        allMenuList.stream().filter(m -> m.getExtraObject() != null)
                .filter(m -> m.getExtraObject().getRouterFlowEntryId() != null)
                .forEach(m -> tokenData.getFlowEntryIds().add(m.getExtraObject().getRouterFlowEntryId()));
        SaSession session = StpUtil.getTokenSession();
        redissonClient.getBucket(tokenData.getMySessionId())
                .set(JSON.toJSONString(tokenData), this.calcSessionExpiredSeconds(), TimeUnit.SECONDS);
        session.set(TokenData.REQUEST_ATTRIBUTE_NAME, tokenData);
    }

    private TokenData loginAndCreateToken(SysUser user) {
        String deviceType = MyCommonUtil.getDeviceTypeWithString();
        LoginUserInfo userInfo = BeanUtil.copyProperties(user, LoginUserInfo.class);
        String loginId = SaTokenUtil.makeLoginId(userInfo);
        SaLoginModel loginModel = new SaLoginModel();
        loginModel.setTimeout(this.calcSessionExpiredSeconds());
        loginModel.setDevice(deviceType);
        StpUtil.login(loginId, loginModel);
        SaSession session = StpUtil.getTokenSession();
        TokenData tokenData = this.buildTokenData(user, session.getId(), StpUtil.getLoginDevice());
        String mySessionId = RedisKeyUtil.getSessionIdPrefix(tokenData, user.getLoginName()) + MyCommonUtil.generateUuid();
        tokenData.setMySessionId(mySessionId);
        tokenData.setToken(session.getToken());
        if (user.getDeptId() != null) {
            SysDept dept = sysDeptService.getById(user.getDeptId());
            if (dept != null) {
                tokenData.setDeptName(dept.getDeptName());
            }
        }
        redissonClient.getBucket(mySessionId)
                .set(JSON.toJSONString(tokenData), this.calcSessionExpiredSeconds(), TimeUnit.SECONDS);
        session.set(TokenData.REQUEST_ATTRIBUTE_NAME, tokenData);
        return tokenData;
    }

    private JSONObject createResponseData(SysUser user, TokenData tokenData) {
        JSONObject jsonData = new JSONObject();
        jsonData.put(TokenData.REQUEST_ATTRIBUTE_NAME, StpUtil.getTokenValue());
        jsonData.put(SHOW_NAME_FIELD, user.getShowName());
        jsonData.put(IS_ADMIN, user.getUserType() == SysUserType.TYPE_ADMIN);
        jsonData.put("userId", user.getUserId());
        jsonData.put("loginName", user.getLoginName());
        if (user.getDeptId() != null) {
            jsonData.put("deptId", tokenData.getDeptId());
            jsonData.put("deptName", tokenData.getDeptName());
        }
        if (StrUtil.isNotBlank(user.getHeadImageUrl())) {
            jsonData.put(HEAD_IMAGE_URL_FIELD, user.getHeadImageUrl());
        }
        return jsonData;
    }

    private void appendResponseMenuAndPermCodeData(
            JSONObject responseData, Collection<SysMenu> allMenuList, Collection<String> menuCodeList) {
        allMenuList.stream()
                .filter(m -> m.getExtraObject() != null && StrUtil.isNotBlank(m.getExtraObject().getMenuCode()))
                .forEach(m -> CollUtil.addAll(menuCodeList, m.getExtraObject().getMenuCode()));
        List<SysMenu> menuList = allMenuList.stream()
                .filter(m -> m.getMenuType() <= SysMenuType.TYPE_MENU).collect(Collectors.toList());
        responseData.put("menuList", menuList);
        responseData.put("permCodeList", menuCodeList);
        TokenData tokenData = TokenData.takeFromRequest();
        if (CollUtil.isNotEmpty(tokenData.getWorkbenchIds())) {
            responseData.put("workbenchIds", tokenData.getWorkbenchIds());
            responseData.put("currentWorkbenchId", tokenData.getWorkbenchId());
        }
    }

    private TokenData buildTokenData(SysUser user, String sessionId, String deviceType) {
        TokenData tokenData = new TokenData();
        tokenData.setSessionId(sessionId);
        Long userId = user.getUserId();
        tokenData.setUserId(userId);
        //通过userid拿到数据权限id并存入tokenData
        String dataPermId =sysDataPermService.queryDataPermIdByUserId(userId);
        tokenData.setDataPermId(dataPermId);
        tokenData.setDeptId(user.getDeptId());
        tokenData.setLoginName(user.getLoginName());
        tokenData.setShowName(user.getShowName());
        tokenData.setIsAdmin(user.getUserType().equals(SysUserType.TYPE_ADMIN));
        tokenData.setLoginIp(IpUtil.getRemoteIpAddress(ContextUtil.getHttpRequest()));
        tokenData.setLoginTime(new Date());
        tokenData.setDeviceType(deviceType);
        tokenData.setHeadImageUrl(user.getHeadImageUrl());
        tokenData.setUserStatus(user.getUserStatus());
        List<SysRole> roleList = null;
        List<SysUserRole> userRoleList = sysRoleService.getSysUserRoleListByUserId(user.getUserId());
        if (CollUtil.isNotEmpty(userRoleList)) {
            Set<Long> userRoleIdSet = userRoleList.stream().map(SysUserRole::getRoleId).collect(Collectors.toSet());
            tokenData.setRoleIds(StrUtil.join(",", userRoleIdSet));
            roleList = sysRoleService.getInList(userRoleIdSet);
        }
        if (tokenData.getIsAdmin().equals(Boolean.TRUE)) {
            roleList = sysRoleService.getAllList();
        }
        if (roleList != null && !roleList.isEmpty()) {
            Set<Long> workbenchIds = roleList.stream()
                    .map(SysRole::getWorkbenchId).filter(Objects::nonNull).collect(Collectors.toSet());
            List<ReportPage> reportPages = reportPageService.getInList(workbenchIds);
            workbenchIds = reportPages.stream().map(ReportPage::getPageId).collect(Collectors.toSet());
            tokenData.setWorkbenchIds(workbenchIds);
            if (user.getWorkbenchId() != null && CollUtil.contains(workbenchIds, user.getWorkbenchId())) {
                tokenData.setWorkbenchId(user.getWorkbenchId());
            } else {
                tokenData.setWorkbenchId(null);
            }
        }
        return tokenData;
    }

    private void putUserSysPermCache(String sessionId, Collection<String> permUrlSet) {
        if (CollUtil.isEmpty(permUrlSet)) {
            return;
        }
        String sessionPermKey = RedisKeyUtil.makeSessionPermIdKey(sessionId);
        RSet<String> redisPermSet = redissonClient.getSet(sessionPermKey);
        redisPermSet.addAll(permUrlSet);
        redisPermSet.expire(this.calcSessionExpiredSeconds(), TimeUnit.SECONDS);
    }

    private void putUserSysPermCodeCache(String sessionId, Collection<String> permCodeSet) {
        if (CollUtil.isEmpty(permCodeSet)) {
            return;
        }
        String sessionPermCodeKey = RedisKeyUtil.makeSessionPermCodeKey(sessionId);
        RSet<String> redisPermSet = redissonClient.getSet(sessionPermCodeKey);
        redisPermSet.addAll(permCodeSet);
        redisPermSet.expire(this.calcSessionExpiredSeconds(), TimeUnit.SECONDS);
    }

    private OnlinePermData getOnlineMenuPermData(Collection<SysMenu> allMenuList) {
        List<SysMenu> onlineMenuList = allMenuList.stream()
                .filter(m -> m.getOnlineFormId() != null && m.getMenuType().equals(SysMenuType.TYPE_BUTTON))
                .collect(Collectors.toList());
        if (CollUtil.isEmpty(onlineMenuList)) {
            return new OnlinePermData();
        }
        Set<Long> formIds = allMenuList.stream()
                .filter(m -> m.getOnlineFormId() != null
                        && m.getOnlineFlowEntryId() == null
                        && m.getMenuType().equals(SysMenuType.TYPE_MENU))
                .map(SysMenu::getOnlineFormId)
                .collect(Collectors.toSet());
        Set<Long> viewFormIds = onlineMenuList.stream()
                .filter(m -> m.getOnlineMenuPermType() == SysOnlineMenuPermType.TYPE_VIEW)
                .map(SysMenu::getOnlineFormId)
                .collect(Collectors.toSet());
        Set<Long> editFormIds = onlineMenuList.stream()
                .filter(m -> m.getOnlineMenuPermType() == SysOnlineMenuPermType.TYPE_EDIT)
                .map(SysMenu::getOnlineFormId)
                .collect(Collectors.toSet());
        // 当前菜单所指向的在线表单，如果存在跳转的目标表单，需要在这里把目标表单的全部读写权限都加入。
        List<Long> jumpToFormIds = allMenuList.stream()
                .filter(m -> m.getOnlineFormId() != null
                        && m.getOnlineFlowEntryId() == null
                        && m.getMenuType().equals(SysMenuType.TYPE_MENU)
                        && m.getExtraObject() != null
                        && m.getExtraObject().getJumpToFormIds() != null)
                .flatMap(m -> m.getExtraObject().getJumpToFormIds().stream())
                .collect(Collectors.toList());
        CollUtil.addAll(formIds, jumpToFormIds);
        CollUtil.addAll(viewFormIds, jumpToFormIds);
        CollUtil.addAll(editFormIds, jumpToFormIds);
        Map<String, Object> permDataMap =
                onlineOperationService.calculatePermData(formIds, viewFormIds, editFormIds);
        OnlinePermData permData = BeanUtil.mapToBean(permDataMap, OnlinePermData.class, false, null);
        permData.permUrlSet.addAll(permData.onlineWhitelistUrls);
        // 当前菜单所指向的在线表单，如果存在对于在线表单后台脚本的调用，需要在这里把目标路径的全部读写权限都加入。
        List<String> extendScripts = allMenuList.stream()
                .filter(m -> m.getMenuType().equals(SysMenuType.TYPE_MENU)
                        && m.getExtraObject() != null
                        && m.getExtraObject().getExtendScripts() != null)
                .flatMap(m -> m.getExtraObject().getExtendScripts().stream())
                .collect(Collectors.toList());
        extendScripts.forEach(scriptCode -> permData.permUrlSet.add("/admin/online/onlineApi/runScript/" + scriptCode));
        return permData;
    }

    private OnlinePermData getFlowOnlineMenuPermData(Collection<SysMenu> allMenuList) {
        List<SysMenu> flowOnlineMenuList = allMenuList.stream()
                .filter(m -> m.getOnlineFlowEntryId() != null).collect(Collectors.toList());
        Set<Long> entryIds = flowOnlineMenuList.stream()
                .map(SysMenu::getOnlineFlowEntryId).collect(Collectors.toSet());
        List<Map<String, Object>> flowPermDataList = flowOnlineOperationService.calculatePermData(entryIds);
        List<OnlineFlowPermData> flowOnlinePermDataList =
                MyModelUtil.mapToBeanList(flowPermDataList, OnlineFlowPermData.class);
        OnlinePermData permData = new OnlinePermData();
        flowOnlinePermDataList.forEach(perm -> {
            permData.permCodeSet.addAll(perm.getPermCodeList());
            permData.permUrlSet.addAll(perm.getPermList());
        });
        return permData;
    }

    private Set<String> getReportMenuPermData(Collection<SysMenu> allMenuList) {
        Set<String> permSet = new HashSet<>();
        List<SysMenu> reportMenuList = allMenuList.stream()
                .filter(m -> m.getReportPageId() != null).collect(Collectors.toList());
        if (CollUtil.isEmpty(reportMenuList)) {
            return permSet;
        }
        Set<Long> pageIds = reportMenuList.stream().map(SysMenu::getReportPageId).collect(Collectors.toSet());
        Map<Long, Set<String>> reportPermDataMap = reportOperationService.calculatePermData(pageIds);
        for (Long pageId : pageIds) {
            CollUtil.addAll(permSet, reportPermDataMap.get(pageId));
        }
        return permSet;
    }

    static class OnlinePermData {
        public final Set<String> permCodeSet = new HashSet<>();
        public final Set<String> permUrlSet = new HashSet<>();
        public final List<String> onlineWhitelistUrls = new LinkedList<>();
    }

    @Data
    static class OnlineFlowPermData {
        private List<String> permCodeList;
        private List<String> permList;
    }
}
